﻿//-----------------------------------------------------------------------
// <copyright file="JobManager.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.Base.Threading.Job
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Threading;
    using TileTool.Log;
    using TileTool.Threading.Job;

    /// <summary>
    /// Simple implementation of the IJobManager interface which uses
    /// the standard .NET ThreadPool class as its source for threads.
    /// 
    /// This means that even though we may have an empty active job list,
    /// we still may not be able to get a thread from the thread pool to
    /// execute on.
    /// </summary>
    public class JobManager : IJobManager
    {
        #region Constants

        /// <summary>
        /// Log group we will log to.
        /// </summary>
        private static string logGroup = typeof(JobManager).Name;

        #endregion

        #region Fields - Private

        /// <summary>
        /// Max number of threads we can use.
        /// </summary>
        private int maxThreads = 0;

        /// <summary>
        /// Locking object used to lock access to the pending and active lists.
        /// </summary>
        private object listLock = new object();

        /// <summary>
        /// Queue of jobs that are waiting to be executed.
        /// </summary>
        private IList<IJob> pendingJobs = new List<IJob>();

        /// <summary>
        /// List of jobs that are actively executing.
        /// </summary>
        private IList<IJob> activeJobs = new List<IJob>();

        #endregion

        #region Constructor

        /// <summary>
        /// Construct the job manager.
        /// </summary>
        /// <param name="maxThreads">Max number of threads we can have running at any time.</param>
        public JobManager(int maxThreads)
        {
            if (maxThreads <= 0)
            {
                throw new ArgumentException("Max threads must be > 0", "maxThreads");
            }

            this.maxThreads = maxThreads;
        }

        #endregion

        #region Properties - Public

        /// <summary>
        /// Gets the number of jobs that are waiting to be executed.
        /// </summary>
        public int PendingJobs
        {
            get
            {
                lock (this.listLock)
                {
                    return this.pendingJobs.Count;
                }
            }
        }

        /// <summary>
        /// Gets the number of jobs that are currently executing.
        /// </summary>
        public int ActiveJobs
        {
            get
            {
                lock (this.listLock)
                {
                    return this.activeJobs.Count;
                }
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Submit the passed job to the job manager to be executed.
        /// </summary>
        /// <param name="job">Job to execute.</param>
        public void Submit(IJob job)
        {
            if (job == null)
            {
                throw new ArgumentNullException("job", "Job must be non-null.");
            }

            lock (this.listLock)
            {
                if (this.activeJobs.IndexOf(job) != -1)
                {
                    throw new ArgumentException("Job is already in active job list.", "job");
                }

                if (this.pendingJobs.IndexOf(job) != -1)
                {
                    throw new ArgumentException("Job is already in pending job list.", "job");
                }

                if (job.Future == null)
                {
                    throw new ArgumentException("Job must have a future object.", "job");
                }

                this.pendingJobs.Add(job);

                LogSystem.LogInfo(
                    logGroup,
                    "Added job to pending: {0} Active: {1} Pending: {2}",
                    job.GetType().Name,
                    this.activeJobs.Count,
                    this.pendingJobs.Count);
            }

            this.ProcessPendingJobs();
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Process any pending job by moving them from the pending list to
        /// the active list. We may not have enough threads to move an item from
        /// pending to active, so this function may simply return.
        /// </summary>
        private void ProcessPendingJobs()
        {
            lock (this.listLock)
            {
                if (this.activeJobs.Count == this.maxThreads)
                {
                    return;
                }

                if (this.pendingJobs.Count == 0)
                {
                    return;
                }

                IJob job = this.pendingJobs[0];

                this.pendingJobs.RemoveAt(0);
                this.activeJobs.Add(job);

                LogSystem.LogInfo(
                    logGroup,
                    "Removed job for execute: {0} Active: {1} Pending: {2}",
                    job.GetType().Name,
                    this.activeJobs.Count,
                    this.pendingJobs.Count);

                this.ExecuteJob(job);
            }
        }

        /// <summary>
        /// Given the passed job, execute it on a thread as soon as one is available.
        /// </summary>
        /// <param name="job">Job to execute.</param>
        private void ExecuteJob(IJob job)
        {
            ThreadPool.QueueUserWorkItem(this.JobExecuteFunction, job);
        }

        /// <summary>
        /// Called when a job has finished executing.
        /// 
        /// This function will remove the passed job from the active job list
        /// and trigger a process pending jobs request.
        /// </summary>
        /// <param name="job">Job that just completed.</param>
        private void CompleteJob(IJob job)
        {
            Debug.Assert(job != null, "Job must be non-null.");

            lock (this.listLock)
            {
                int jobIndex = this.activeJobs.IndexOf(job);
                if (jobIndex == -1)
                {
                    LogSystem.LogError(logGroup, "Job completed, but not found in active job list!");
                    return;
                }

                this.activeJobs.RemoveAt(jobIndex);

                LogSystem.LogInfo(
                    logGroup,
                    "Job completed: {0} Active: {1} Pending: {2}",
                    job.GetType().Name,
                    this.activeJobs.Count,
                    this.pendingJobs.Count);
            }

            this.ProcessPendingJobs();
        }

        /// <summary>
        /// Function called from the ThreadPool which is used to execute a job.
        /// 
        /// Any exceptions thrown by the job will be caught and logged.
        /// </summary>
        /// <param name="state">User provided state data. In this case, or IJob.</param>
        private void JobExecuteFunction(object state)
        {
            IJob job = state as IJob;

            try
            {
                LogSystem.LogInfo(logGroup, "Executing job: {0} Thread: {1}", job.GetType().Name, Thread.CurrentThread.ManagedThreadId);
                job.Execute();
            }
            catch (Exception e)
            {
                LogSystem.LogError(logGroup, "Job threw exception: {0}", e.ToString());

                if (job.Future != null)
                {
                    job.Future.Cancel();
                }
            }

            this.CompleteJob(job);
        }

        #endregion
    }
}
